#ifndef AEYECLIENTS_AEYESENSORSDK_INCLUDEDEV_SERIALIZE_HPP_
#define AEYECLIENTS_AEYESENSORSDK_INCLUDEDEV_SERIALIZE_HPP_

#include <cereal/cereal.hpp>
#include <cereal/types/vector.hpp>
#include <cereal/types/string.hpp>

#include "PointCloudFrame.hpp"
#include "PointStructure.hpp"

// brief overview:
// `cereal` provides two(ish) ways to (de)serialize structs - the serialize function and the save/load pair. for full
// details check their docs, but relevant to this is that the use of ATTRIBUTE_PACKED _does_ force us to use a
// save/load pair for the structs, because it is illegal to take a pointer to or reference of a field in a packed
// struct. to work around this, the `save` function makes a local copy of each Point field (via the macro seen below),
// before serializing that. the `load` function does the same in reverse, making a local variable that gets filled in
// with copies of the values from the archive and then using them to construct a new Point with the deserialized values
// over the one the Point that was passed in to the function.

// in order to avoid leaking the cereal libraries into customer-facing headers, these are implemented as external
// serialization functions. however, in order to serialize pointcloud frames (which have all private fields that are
// accessed via getters and setters), as of now it's easiest to just make a new struct with the same fields and then
// serialize that, converting where needed. because of move constructors there should be very little overhead for
// moving the vector<AEyePoint>, especially compared to the speed of (de)serialization.

namespace aeye {

// these macros consolidate some very very repetitive code while also reducing the probability for typos in serialization
// code. 'auto' means that you can't accidentally do something weird with type conversions, and the macro argument has
// to be a valid field of the relevant 'point' object to expand to valid code, so typos should be caught at compile
// time, while the correct name is also passed in for the name-value pair when serializing to a text archive.

#define ARCHIVE_POINTS_2(_1, _2)                                                        \
do {                                                                                    \
  auto local_var_##_1 = point._1;                                                       \
  auto local_var_##_2 = point._2;                                                       \
  ar(::cereal::make_nvp(#_1, local_var_##_1), ::cereal::make_nvp(#_2, local_var_##_2)); \
} while (0)

#define ARCHIVE_POINTS_3(_1, _2, _3)                                                   \
do {                                                                                   \
  auto local_var_##_1 = point._1;                                                      \
  auto local_var_##_2 = point._2;                                                      \
  auto local_var_##_3 = point._3;                                                      \
  ar(::cereal::make_nvp(#_1, local_var_##_1), ::cereal::make_nvp(#_2, local_var_##_2), \
     ::cereal::make_nvp(#_3, local_var_##_3));                                         \
} while (0)

namespace detail {
// small helper: call archive (arg 1)'s operator() with all the variadic args to read the value into each of them
// the variadic args are then used to construct (replace) the Point (arg 2) with the deserialized values
template<typename Archive, typename Point, typename... Args>
inline void read(Archive& archive, Point& point, Args... args) {
  archive(args...);
  point = {args...};
}
}

template<class Archive>
inline void save(Archive& ar, const PointReturnCartesianCoords & point) {
  ARCHIVE_POINTS_3(x, y, z);
}
template<class Archive>
inline void load(Archive& ar, PointReturnCartesianCoords& point) {
  float x{}, y{}, z{};
  detail::read(ar, point, x, y, z);
}
template<class Archive>
inline void save(Archive &ar, const PointReturnSphericalCoords &point) {
  ARCHIVE_POINTS_3(azimuth, elevation, radius);
}
template<class Archive>
inline void load(Archive& ar, PointReturnSphericalCoords &point) {
  int16_t azimuth{}, elevation{};
  float radius{};
  detail::read(ar, point, azimuth, elevation, radius);
}
template<class Archive>
inline void save(Archive &ar, const PointReturnUnused_1 &point) {
  ARCHIVE_POINTS_3(reserved1, reserved2, flags);
}
template<class Archive>
inline void load(Archive &ar, PointReturnUnused_1 &point) {
  uint16_t reserved1{}, reserved2{};
  uint8_t flags{};
  detail::read(ar, point, reserved1, reserved2, flags);
}
template<class Archive>
inline void save(Archive &ar, const PointReturnIntensity &point) {
  ARCHIVE_POINTS_2(reported_raw_intensity, quasi_reflectivity);
}
template<class Archive>
inline void load(Archive &ar, PointReturnIntensity &point) {
  uint16_t raw_intensity{}, quasi_reflectivity{};
  detail::read(ar, point, raw_intensity, quasi_reflectivity);
}
template<class Archive>
inline void save(Archive &ar, const PointReturnUnused_2 &point) {
  ARCHIVE_POINTS_2(frame_id, reserved1);
}
template<class Archive>
inline void load(Archive &ar, PointReturnUnused_2 &point) {
  uint32_t frame_id{}, reserved1{};
  detail::read(ar, point, frame_id, reserved1);
}
template<class Archive>
inline void save(Archive &ar, const PointReturnUnused_3 &point) {
  ARCHIVE_POINTS_2(reserved1, reserved2);
}
template<class Archive>
inline void load(Archive &ar, PointReturnUnused_3 &point) {
  uint16_t reserved1{}, reserved2{};
  detail::read(ar, point, reserved1, reserved2);
}
template<class Archive>
inline void save(Archive &ar, const PointReturnUnused_4 &point) {
  ARCHIVE_POINTS_2(reserved1, reserved2);
}
template<class Archive>
inline void load(Archive &ar, PointReturnUnused_4 &point) {
  uint16_t reserved1{}, reserved2{};
  detail::read(ar, point, reserved1, reserved2);
}
template<class Archive>
inline void save(Archive &ar, const PointReturnUnused_5 &point) {
  ARCHIVE_POINTS_2(reserved1, reserved2);
}
template<class Archive>
inline void load(Archive &ar, PointReturnUnused_5 &point) {
  uint16_t reserved1{}, reserved2{};
  detail::read(ar, point, reserved1, reserved2);
}
template<class Archive>
inline void save(Archive& ar, const PointReturnUnused_6& point) {
  ARCHIVE_POINTS_3(reserved1, reserved2, reserved3);
}
template<class Archive>
inline void load(Archive& ar, PointReturnUnused_6& point) {
  uint8_t reserved1{}, reserved2{}, reserved3{};
  detail::read(ar, point, reserved1, reserved2, reserved3);
}
// minimal save/load make text representation "point: value" rather than "point: { key0: value0 }"
// irrelevant for binary archives, but still fully functional
template<class Archive>
inline uint32_t save_minimal(const Archive &, const PointReturnType &point) {
  return point.point_return_mask;
}
template<class Archive>
inline void load_minimal(const Archive &, PointReturnType &point, const uint32_t &val) {
  point.point_return_mask = val;
}
template<class Archive>
inline void serialize(Archive &archive, AEyePoint &point) {
  // do note that these need to be references for deserialization to work properly
  // additionally, CEREAL_NVP passes variable names as key/value pair names, so changing the name of one of these
  // will change how input and output need to flow
  auto &cartesian = point.pointReturnCartesianCoords;
  auto &spherical = point.pointReturnSphericalCoords;
  auto &unused_1 = point.pointReturnUnused_1;
  auto &intensity = point.pointReturnIntensity;
  auto &unused_2 = point.pointReturnUnused_2;
  auto &unused_3 = point.pointReturnUnused_3;
  auto &unused_4 = point.pointReturnUnused_4;
  auto &unused_5 = point.pointReturnUnused_5;
  auto &unused_6 = point.pointReturnUnused_6;
  auto &type = point.pointReturnType;
  archive(
      CEREAL_NVP(cartesian), CEREAL_NVP(spherical), CEREAL_NVP(unused_1),
      CEREAL_NVP(intensity), CEREAL_NVP(unused_2), CEREAL_NVP(unused_3),
      CEREAL_NVP(unused_4), CEREAL_NVP(unused_5), CEREAL_NVP(unused_6), CEREAL_NVP(type)
  );
}


struct serialize_frame { // NOLINT
  int                               frameId{0};          ///< Frame Id
  std::string                       sensorIp;            ///< Sensor Ip Address
  uint32_t                          tsSec;               ///< Frame timestamp seconds
  uint32_t                          tsuSec;              ///< Frame timestamp microseconds
  uint16_t                          pointReturnFlags{0}; ///< Frame point returns flag
  std::vector<AEyePoint>            points{};
  size_t                            numPointsReportedBySensor{0};
  serialize_frame() = default; // cereal needs access to default ctor

  explicit serialize_frame(aeye::PointCloudFrame&& p)
      : frameId(p.getFrameId())
      , sensorIp(std::move(p.getIpAddress()))
      , tsSec(p.getTimeSec())
      , tsuSec(p.getTimeuSec())
      , pointReturnFlags(p.getPointReturnFlags())
      , points(std::move(p.getPoints()))
      , numPointsReportedBySensor(p.getNumExpectedPoints()) {}

  template<class Archive>
  void serialize(Archive &archive) {
    archive(CEREAL_NVP(frameId), CEREAL_NVP(sensorIp), CEREAL_NVP(tsSec),
            CEREAL_NVP(tsuSec), CEREAL_NVP(pointReturnFlags),
            CEREAL_NVP(points), CEREAL_NVP(numPointsReportedBySensor));
  }
};

#undef ARCHIVE_POINTS_2
#undef ARCHIVE_POINTS_3

}

#endif //AEYECLIENTS_AEYESENSORSDK_INCLUDEDEV_SERIALIZE_HPP_
